查看原文
其他

应用AST技术实现自动化升级React 15至React 16的解决方案

王亮 58技术 2022-03-15


导语

本文通过React语法从v15自动升级为v16的方案,阐述了AST的概念及其在前端项目中的应用与探索,介绍了若干适合AST技术在前端落地的场景。



背景

通常一个中后台系统至少有三到五年的生命周期。 

在立项之初一般会采用一些成熟稳健的技术,然而随着时间的流逝,原有技术栈必将逐渐变得老旧、难以维护:比如 vue1.x 升级至 2.x、swift的历次升级。 

那么我们应当如何处理这些老旧的代码,是另起炉灶推倒重来,亦或是硬着头皮在原来的基础上继续打补丁? 

对于上述问题,本文主要介绍一种基于修改抽象语法树(Abstract Syntax Tree,AST)实现自动化升级老旧项目技术栈的方案。案例为React 15 升级至 React 16。

应用AST技术实现自动化升级React技术栈的解决方案

1、方案概述

该方案的主要步骤如下:

  • 通过原始代码生成 AST;
  • 按照一定规则修改裁剪 AST;
  • 根据修改后的 AST 生成新代码。
该方案所需解决的主要问题如下:
  • 模块的引入/导出改为 ES6 module 的方式;
  • React15 的 createClass 语法改为 React16 的 class 组件:其中需要重点关注的内容有:
        a)class语法转换;
        b)createClass组件内函数 this 自绑定转换;
        c)getInitialState和getDefaultProps转换为新的state和props声明;
  • 部分生命周期如 componentWillMount 修改为 UNSAFE_componentWillMount;
确定了方案和问题后,让我们一起揭开AST的面纱。


2、初识AST
我们知道,即使是解释执行的语言,也是需要编译的。词法分析会把语句分解成词法单元,即 Token。语法分析会把 Token 转化成抽象语法树,即 AST。





AST流水线
通过工具astexplorer可以查看查看语句let a = 2 所转换的AST:




let a = 2 生成的AST





树状结构
简单解释一下AST:
  • type值为Program通常表示根节点
  • body是一个包含多个Statement的数组,每一个Statement相当于一个语句
    A)type值为VariableDeclaration说明该语句是个变量声明语句
    B)declarations中:
    C)VariableDeclarator代表变量声明的描述
        a)id代表变量名
        b)init代表变量初始值
    D)kind声明变量类型let
  • sourceType为module代表它是一个模块
完整的AST文档可以参考AST Spec 。


3、使用Babel进行代码转换
实际工程中我们不需要从头去实现词法分析器、语法分析器,因为工程化的前端项目都会用到Babel ,而@babel/core 已经为我们提供了强大的parser、traverse等工具,利用它们可以快速生成并修改AST。
在使用这些工具之前,我们先比较一下原始代码和目标代码:





原始代码与目标代码对比

使用 astexplorer 来将原始代码和期望代码生成AST,进行对比:





AST对比
通过对比可以发现两份AST的结构非常相似。对比语句var React = require("react");和Import React from "react";所生成的Statement可以发现,原始AST稍加修改就能得到目标AST:








首先修改Statement的type,不同的Statement其结构也不尽相同,ImportDeclaration的结构如下:




其中关键部分为specifiers(说明符)和source(源):
  • specifiers表示引入的变量;
  • source表示从哪个源引入。
解析VariableDeclaration可以发现我们需要的specifiers和source在VariableDeclaration中都有对应的部分:
  • specifiers.local对应declarations.id;
  • source对应declarations.init.arguments。
因此稍加修改原始AST就能够转换成目标AST。而所需要修改的部分和我们第一步总结的大同小异:
  • 以类型 VariableDeclaration 声明的 require 语句需要转化为 ImportDeclaration。
  • 以 VariableDeclaration 声明的 createClass 语句其type需要转化为 ClassDeclaration。
  • 在createClass语法中 JSXElement 上使用诸如onClick等事件来调用成员函数时,this是自绑定的,而Class语法中则需要显式绑定:




解决思路是判断节点属性为React事件且node.value.expression.type是 MemberExpression ,则修改 node.value.expression.type 为 CallExpression 并为其增加 callee 属性。
  • 处理getInitialState:删除该节点,将该节点内容增加在 constructor 中。
  • 处理getDefaultProps:删除该节点,将该节点内容添加在最外层 body 中,类型为ExpressionStatement。
  • module.epxorts ExpressionStatement 转为 ExportDefaultDeclaration。
转换流程如下图:




转换流程

部分示例代码如下:





查看输出的代码后发现空格、缩进等格式会有些乱,这是由于我们没有对 start、end 等表示位置的属性进行处理。可以用prettier-eslint进行处理以获得格式化后的代码(prettier也是基于AST实现的)。 
这样就实现了老项目焕发新生。


4、实践项目
信安猎人人工审核系统始建于2016年,该系统前端部分至2018年底共计有9 个人工审核模板(即子系统),接入300余个场景。全部模板均fork了采用 React15 的 createClass 语法编写,通过 webpack2 进行构建的初始模板。随着产品功能的不断演进、接入场景的不断增加,在维护该项目的过程中我们总结了几个亟待解决的问题:
  • 我们在产品技术演进过程中为保证中台系统体验的一致性积累了许多业务组件,因为采用了React16 hooks等特性导致不能在该系统中直接复用,复用成本高;
  • 系统所用到的公共组件散落在各个模板中进行维护,维护成本高;
  • 代码语法与主流技术栈产生了一定程度的割裂,不利于后续维护。
应用该方案升级改造后以上几个问题都得到了解决:
  • 升级后可以直接复用部分业务组件,降低了复用成本、也降低了后续的维护成本;
  • 部分组件可以用业务组件代替、另外部分组件可以提取到业务组件库,进行统一管理,提高了复用率,降低了维护成本;
  • 代码语法已经升级到React16,降低了维护成本。


AST的其它应用场景

回到AST技术本身,其应用场景是非常广泛的,甚至可以说是前端项目的基石:Babel转译、代码压缩、PostCSS、lint等工具都是基于AST。以下列举几个适合应用AST技术的场景:

1、跨平台/框架组件互转
在小程序百花齐放的今天,我们的业务需要支持各个小程序平台。如果采用原生开发方案,那么单一功能/组件需要在各个平台重复实现,不仅开发效率低,维护成本也成倍增加。使用AST进行转换可以显著帮助开发者降低开发维护成本。
对比不同小程序的模板文件、样式处理以及属性、事件、生命周期,并且统计出功能近似的部分。通过对比我们可以发现,各个平台提供的主要能力大部分都是接近的,这就是我们能够通过AST进行小程序转换的基础。
另外,在苹果向开发者发布“更新使用网页视图的App”通知的前提下,许多大量使用WebView的App都需要进行更新。如果将原Webview功能用Objective-C/Swift重新实现,那么成本对于大多数团队都高到无法接受。这个时候我们可以考虑使用ReactNative来替换原有Webview,通过AST来自动转换React/Vue组件为ReactNative组件,可以极大的降低切换成本。当然这个方案也有其自身的局限性:
  • React和Vue存在部分生命周期、高阶函数、Fragment等无法兼容之处;
  • ReactNative的样式仅能支持部分CSS子集,部分样式可能需要修改;
  • 部分Web组件需要重新开发为ReactNative组件。
虽然跨平台/跨框架的组件转换方案仍然存在着若干不足,但是对于以上两个适用的场景,效率的提升还是非常明显的。

2、结合Markdown生成时序图

当我们接手维护或者扩展一个业务项目时,如果其实现逻辑相对复杂,那么即使有完善的文档、注释来支撑,在修改时也需要花费一定的时间精力去梳理。这时候我们可以通过AST获取函数调用关系,再将其通过Markdown生成为时序图。通过时序图来协助我们理清调用逻辑。示例如下:





Markdown代码





上述代码生成的时序图


3、webpack插件
举例来说:在开发环境中我们通常会增加许多调试相关的代码,而这些代码对于业务逻辑并没有价值,因此有必要在生产环境构建时移除掉这些代码,而手动移除费时费力还容易疏漏。这时就可以编写webpack插件,通过AST来找到debug相关代码并将其移除,既能保证生产环境的清洁,又能在开发环境充分利用debug信息。

总结

通过对以上几个方案的分析我们可以看出,所有的解决方案都有其闪光/不足之处,我们工程师的职责就是根据不同的场景定制相对最优的解决方案。这也应了软件工程中的那句老话:“没有银弹”。
随着AST在前端的应用场景越来越多,它的重要性也不断提升,因此前端工程师也有必要掌握AST相关知识,在适合的场景运用这项技术来解决问题。对此,希望这篇文章能够帮助到你。Happy Hacking !

参考文献

1、AST Spec 
2、babel-parser/ babel-traverse


作者简介

王亮,本地服务事业群前端工程师,具备JavaScript、Java、Python、Objective-C等语言开发经验。相信并践行“职位有前后端之分但工程师没有”。


END

阅读推荐

深度文本表征与深度文本聚类在小样本场景中的探索与实践
前端爬虫攻防之接口签名方案
58商家通Android端WebView加载优化方案
并发在58二手车列表的应用
58App-Android端的动态化框架实践与思考
API管理平台之SCF服务测试篇









您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存